<?php

namespace WPGraphQL\Mutation;

use GraphQL\Deferred;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Data\PostObjectMutation;

class PostObjectCreate {
	/**
	 * Registers the PostObjectCreate mutation.
	 *
	 * @param \WP_Post_Type $post_type_object   The post type of the mutation.
	 */
	public static function register_mutation( \WP_Post_Type $post_type_object ) {
		$mutation_name = 'create' . ucwords( $post_type_object->graphql_single_name );

		register_graphql_mutation(
			$mutation_name,
			[
				'inputFields'         => self::get_input_fields( $post_type_object ),
				'outputFields'        => self::get_output_fields( $post_type_object ),
				'mutateAndGetPayload' => self::mutate_and_get_payload( $post_type_object, $mutation_name ),
			]
		);
	}

	/**
	 * Defines the mutation input field configuration.
	 *
	 * @param \WP_Post_Type $post_type_object   The post type of the mutation.
	 *
	 * @return array
	 */
	public static function get_input_fields( $post_type_object ) {
		$fields = [
			'authorId'      => [
				'type'        => 'ID',
				'description' => __( 'The userId to assign as the author of the post', 'wp-graphql' ),
			],
			'commentCount'  => [
				'type'        => 'INT',
				'description' => __( 'The number of comments. Even though WPGraphQL denotes this field as an integer, in WordPress this field should be saved as a numeric string for compatibility.', 'wp-graphql' ),
			],
			'commentStatus' => [
				'type'        => 'String',
				'description' => __( 'The comment status for the object', 'wp-graphql' ),
			],
			'content'       => [
				'type'        => 'String',
				'description' => __( 'The content of the object', 'wp-graphql' ),
			],
			'date'          => [
				'type'        => 'String',
				'description' => __( 'The date of the object. Preferable to enter as year/month/day (e.g. 01/31/2017) as it will rearrange date as fit if it is not specified. Incomplete dates may have unintended results for example, "2017" as the input will use current date with timestamp 20:17 ', 'wp-graphql' ),
			],
			'excerpt'       => [
				'type'        => 'String',
				'description' => __( 'The excerpt of the object', 'wp-graphql' ),
			],
			'menuOrder'     => [
				'type'        => 'Int',
				'description' => __( 'A field used for ordering posts. This is typically used with nav menu items or for special ordering of hierarchical content types.', 'wp-graphql' ),
			],
			'mimeType'      => [
				'type'        => 'MimeTypeEnum',
				'description' => __( 'If the post is an attachment or a media file, this field will carry the corresponding MIME type. This field is equivalent to the value of WP_Post->post_mime_type and the post_mime_type column in the "post_objects" database table.', 'wp-graphql' ),
			],
			'parentId'      => [
				'type'        => 'Id',
				'description' => __( 'The ID of the parent object', 'wp-graphql' ),
			],
			'password'      => [
				'type'        => 'String',
				'description' => __( 'The password used to protect the content of the object', 'wp-graphql' ),
			],
			'pinged'        => [
				'type'        => [
					'list_of' => 'String',
				],
				'description' => __( 'URLs that have been pinged.', 'wp-graphql' ),
			],
			'pingStatus'    => [
				'type'        => 'String',
				'description' => __( 'The ping status for the object', 'wp-graphql' ),
			],
			'slug'          => [
				'type'        => 'String',
				'description' => __( 'The slug of the object', 'wp-graphql' ),
			],
			'status'        => [
				'type'        => 'PostStatusEnum',
				'description' => __( 'The status of the object', 'wp-graphql' ),
			],
			'title'         => [
				'type'        => 'String',
				'description' => __( 'The title of the post', 'wp-graphql' ),
			],
			'toPing'        => [
				'type'        => [
					'list_of' => 'String',
				],
				'description' => __( 'URLs queued to be pinged.', 'wp-graphql' ),
			],
		];

		$allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies();
		if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
			foreach ( $allowed_taxonomies as $taxonomy ) {
				// If the taxonomy is in the array of taxonomies registered to the post_type
				if ( in_array( $taxonomy, get_object_taxonomies( $post_type_object->name ), true ) ) {
					$tax_object = get_taxonomy( $taxonomy );

					register_graphql_input_type(
						$post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
						[
							'description' => sprintf( __( 'List of %1$s to connect the %2$s to. If an ID is set, it will be used to create the connection. If not, it will look for a slug. If neither are valid existing terms, and the site is configured to allow terms to be created during post mutations, a term will be created using the Name if it exists in the input, then fallback to the slug if it exists.', 'wp-graphql' ), $tax_object->graphql_plural_name, $post_type_object->graphql_single_name ),
							'fields'      => [
								'id'          => [
									'type'        => 'Id',
									'description' => sprintf( __( 'The ID of the %1$s. If present, this will be used to connect to the %2$s. If no existing %1$s exists with this ID, no connection will be made.', 'wp-graphql' ), $tax_object->graphql_single_name, $post_type_object->graphql_single_name ),
								],
								'slug'        => [
									'type'        => 'String',
									'description' => sprintf( __( 'The slug of the %1$s. If no ID is present, this field will be used to make a connection. If no existing term exists with this slug, this field will be used as a fallback to the Name field when creating a new term to connect to, if term creation is enabled as a nested mutation.', 'wp-graphql' ), $tax_object->graphql_single_name ),
								],
								'description' => [
									'type'        => 'String',
									'description' => sprintf( __( 'The description of the %1$s. This field is used to set a description of the %1$s if a new one is created during the mutation.', 'wp-graphql' ), $tax_object->graphql_single_name ),
								],
								'name'        => [
									'type'        => 'String',
									'description' => sprintf( __( 'The name of the %1$s. This field is used to create a new term, if term creation is enabled in nested mutations, and if one does not already exist with the provided slug or ID or if a slug or ID is not provided. If no name is included and a term is created, the creation will fallback to the slug field.', 'wp-graphql' ), $tax_object->graphql_single_name ),
								],
							],
						]
					);

					register_graphql_input_type(
						ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
						[
							'description' => sprintf( __( 'Set relationships between the %1$s to %2$s', 'wp-graphql' ), $post_type_object->graphql_single_name, $tax_object->graphql_plural_name ),
							'fields'      => [
								'append' => [
									'type'        => 'Boolean',
									'description' => sprintf( __( 'If true, this will append the %1$s to existing related %2$s. If false, this will replace existing relationships. Default true.', 'wp-graphql' ), $tax_object->graphql_single_name, $tax_object->graphql_plural_name ),
								],
								'nodes'  => [
									'type' => [
										'list_of' => $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
									],
								],
							],
						]
					);

					$fields[ $tax_object->graphql_plural_name ] = [
						'description' => sprintf( __( 'Set connections between the %1$s and %2$s', 'wp-graphql' ), $post_type_object->graphql_single_name, $tax_object->graphql_plural_name ),
						'type'        => ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
					];
				}
			}
		}

		return $fields;
	}

	/**
	 * Defines the mutation output field configuration.
	 *
	 * @param \WP_Post_Type $post_type_object   The post type of the mutation.
	 *
	 * @return array
	 */
	public static function get_output_fields( $post_type_object ) {
		return [
			$post_type_object->graphql_single_name => [
				'type'    => $post_type_object->graphql_single_name,
				'resolve' => function ( $payload, $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) {

					if ( empty( $payload['postObjectId'] ) || ! absint( $payload['postObjectId'] ) ) {
						return null;
					}

					return DataSource::resolve_post_object( $payload['postObjectId'], $context );
				},
			],
		];
	}

	/**
	 * Defines the mutation data modification closure.
	 *
	 * @param \WP_Post_Type $post_type_object   The post type of the mutation.
	 * @param string        $mutation_name      The mutation name.
	 *
	 * @return callable
	 */
	public static function mutate_and_get_payload( $post_type_object, $mutation_name ) {
		return function ( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {

			/**
			 * Throw an exception if there's no input
			 */
			if ( ( empty( $post_type_object->name ) ) || ( empty( $input ) || ! is_array( $input ) ) ) {
				throw new UserError( __( 'Mutation not processed. There was no input for the mutation or the post_type_object was invalid', 'wp-graphql' ) );
			}

			/**
			 * Stop now if a user isn't allowed to create a post
			 */
			if ( ! current_user_can( $post_type_object->cap->create_posts ) ) {
				// translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
				throw new UserError( sprintf( __( 'Sorry, you are not allowed to create %1$s', 'wp-graphql' ), $post_type_object->graphql_plural_name ) );
			}

			/**
			 * If the post being created is being assigned to another user that's not the current user, make sure
			 * the current user has permission to edit others posts for this post_type
			 */
			if ( ! empty( $input['authorId'] ) && get_current_user_id() !== $input['authorId'] && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
				// translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
				throw new UserError( sprintf( __( 'Sorry, you are not allowed to create %1$s as this user', 'wp-graphql' ), $post_type_object->graphql_plural_name ) );
			}

			/**
			 * @todo: When we support assigning terms and setting posts as "sticky" we need to check permissions
			 * @see :https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L504-L506
			 * @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L496-L498
			 */

			/**
			 * insert the post object and get the ID
			 */
			$post_args = PostObjectMutation::prepare_post_object( $input, $post_type_object, $mutation_name );

			/**
			 * Filter the default post status to use when the post is initially created. Pass through a filter to
			 * allow other plugins to override the default (for example, Edit Flow, which provides control over
			 * customizing stati or various E-commerce plugins that make heavy use of custom stati)
			 *
			 * @param string        $default_status   The default status to be used when the post is initially inserted
			 * @param \WP_Post_Type $post_type_object The Post Type that is being inserted
			 * @param string        $mutation_name    The name of the mutation currently in progress
			 */
			$default_post_status = apply_filters( 'graphql_post_object_create_default_post_status', 'draft', $post_type_object, $mutation_name );

			/**
			 * We want to cache the "post_status" and set the status later. We will set the initial status
			 * of the inserted post as the default status for the site, allow side effects to process with the
			 * inserted post (set term object connections, set meta input, sideload images if necessary, etc)
			 * Then will follow up with setting the status as what it was declared to be later
			 */
			$intended_post_status = ! empty( $post_args['post_status'] ) ? $post_args['post_status'] : $default_post_status;

			/**
			 * Set the post_status as the default for the initial insert. The intended $post_status will be set after
			 * side effects are complete.
			 */
			$post_args['post_status'] = $default_post_status;

			/**
			 * Insert the post and retrieve the ID
			 */
			$post_id = wp_insert_post( wp_slash( (array) $post_args ), true );

			/**
			 * Throw an exception if the post failed to create
			 */
			if ( is_wp_error( $post_id ) ) {
				$error_message = $post_id->get_error_message();
				if ( ! empty( $error_message ) ) {
					throw new UserError( esc_html( $error_message ) );
				} else {
					throw new UserError( __( 'The object failed to create but no error was provided', 'wp-graphql' ) );
				}
			}

			/**
			 * If the $post_id is empty, we should throw an exception
			 */
			if ( empty( $post_id ) ) {
				throw new UserError( __( 'The object failed to create', 'wp-graphql' ) );
			}

			/**
			 * This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
			 *
			 * The input for the postObjectMutation will be passed, along with the $new_post_id for the
			 * postObject that was created so that relations can be set, meta can be updated, etc.
			 */
			PostObjectMutation::update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status );

			/**
			 * Determine whether the intended status should be set or not.
			 *
			 * By filtering to false, the $intended_post_status will not be set at the completion of the mutation.
			 *
			 * This allows for side-effect actions to set the status later. For example, if a post
			 * was being created via a GraphQL Mutation, the post had additional required assets, such as images
			 * that needed to be sideloaded or some other semi-time-consuming side effect, those actions could
			 * be deferred (cron or whatever), and when those actions complete they could come back and set
			 * the $intended_status.
			 *
			 * @param boolean       $should_set_intended_status Whether to set the intended post_status or not. Default true.
			 * @param \WP_Post_Type $post_type_object           The Post Type Object for the post being mutated
			 * @param string        $mutation_name              The name of the mutation currently in progress
			 * @param AppContext    $context                    The AppContext passed down to all resolvers
			 * @param ResolveInfo   $info                       The ResolveInfo passed down to all resolvers
			 * @param string        $intended_post_status       The intended post_status the post should have according to the mutation input
			 * @param string        $default_post_status        The default status posts should use if an intended status wasn't set
			 */
			$should_set_intended_status = apply_filters( 'graphql_post_object_create_should_set_intended_post_status', true, $post_type_object, $mutation_name, $context, $info, $intended_post_status, $default_post_status );

			/**
			 * If the intended post status and the default post status are not the same,
			 * update the post with the intended status now that side effects are complete.
			 */
			if ( $intended_post_status !== $default_post_status && true === $should_set_intended_status ) {

				/**
				 * If the post was deleted by a side effect action before getting here,
				 * don't proceed.
				 */
				if ( ! $new_post = get_post( $post_id ) ) {
					throw new UserError( sprintf( __( 'The status of the post could not be set', 'wp-graphql' ) ) );
				}

				/**
				 * If the $intended_post_status is different than the current status of the post
				 * proceed and update the status.
				 */
				if ( $intended_post_status !== $new_post->post_status ) {
					$update_args = [
						'ID'          => $post_id,
						'post_status' => $intended_post_status,
						// Prevent the post_date from being reset if the date was included in the create post $args
						// see: https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/post.php#L3637
						'edit_date'   => ! empty( $post_args['post_date'] ) ? $post_args['post_date'] : false,
					];

					wp_update_post( $update_args );
				}
			}

			/**
			 * Return the post object
			 */
			return [
				'postObjectId' => $post_id,
			];
		};
	}
}
